<?php
/*
 * @Author: 杨红兵 
 * @Date: 2022-09-05 15:24:56 
 * @Last Modified by: 杨红兵
 * @Last Modified time: 2022-09-05 17:09:32
 */

namespace app\libraries\utils\lock;

use app\libraries\utils\traits\Signleton;
use think\facade\Cache;
use think\helper\Str;

class RedisLock implements LockInterface
{
    use Signleton;

    /**
     * 加锁LUA脚本
     */
    public const LUA_LOCK_STR = <<<LUA
local jTable = {
    key = ARGV[1],
    va = 1,
    ex = tonumber(ARGV[2])
}
local x = redis.call('SET', KEYS[1], cjson.encode(jTable), 'NX', 'EX', ARGV[2])
if not x then
    local lTable = cjson.decode(redis.call('GET', KEYS[1]))
    if lTable.key == ARGV[1] then
        local lValue = 1
        if lTable.va ~= nil then
            lValue = tonumber(lTable.va)
        end
        lTable.va = lValue + 1
        local expireValue = tonumber(ARGV[2])
        if lTable.ex ~= nil and tonumber(lTable.ex) > expireValue then
            expireValue = lTable.ex
        else
            lTable.ex = expireValue
        end
        redis.call('SET', KEYS[1], cjson.encode(lTable), 'EX', expireValue)
    end
    return lTable.key
else
    return ARGV[1]
end
LUA;

    /**
     * 释放锁LUA脚本
     */
    public const LUA_UN_LOCK_STR = <<<LUA
local lStr = redis.call('GET', KEYS[1])
if lStr == nil or not lStr then
    return 0
end
local err,lTable = pcall(function(str)return cjson.decode(str)end, lStr)
if err and lTable.key == ARGV[1] then
    if tonumber(ARGV[2]) == 0 and lTable.va ~= nil and lTable.va > 1 then
        lTable.va = lTable.va - 1
        local expireValue = 30
        if lTable.ex ~= nil then
            expireValue = lTable.ex
        end
        redis.call('SET', KEYS[1], cjson.encode(lTable), 'EX', expireValue)
        return 'VALUE:' .. tostring(lTable.va)
    else
        return redis.call("DEL", KEYS[1])
    end
else
    return 0
end
LUA;

    /**
     * 加锁KEY前缀
     *
     * @var string
     * @Author YangHB
     * @DateTime 2022-09-05 17:03:27
     *
     */
    protected string $profix = "lock_";

    /**
     * 统一添加前缀
     *
     * @Author YangHB
     * @DateTime 2022-09-05 17:03:39
     *
     * @param string $key
     * @return string
     */
    protected function getKey(string $key): string
    {
        return $this->profix . $key;
    }

    /**
     * 加锁操作
     *
     * @Author YangHB
     * @DateTime 2022-09-05 17:03:49
     *
     * @param string $key 加锁的KEY
     * @param integer $time 锁时间
     * @param string $lockValue 可重入标识
     * @return string|boolean 返回为字符串时，是锁的内容，用于重入与释放锁资源
     */
    public function lock(string $key, int $time, string $lockValue = "")
    {
        $key = $this->getKey($key);
        $lockValue = empty($lockValue) ? get_sequuid(true) : $lockValue;
        $redis = Cache::store("lock_redis");
        $lockStr = $redis->eval(self::LUA_LOCK_STR, [$key, $lockValue, $time], 1);
        return $lockStr !== $lockValue ? false : $lockValue;
    }

    /**
     * 加锁失败等待
     *
     * @Author YangHB
     * @DateTime 2022-09-05 17:04:00
     *
     * @param string $key
     * @param integer $time
     * @param string $lockValue
     * @param integer $timeOut
     * @return string|boolean
     */
    public function lockWait(string $key, int $time, string $lockValue = "", int $timeOut = 5)
    {
        $key = $this->getKey($key);
        $lockValue = empty($lockValue) ? get_sequuid(true) : $lockValue;
        $redis = Cache::store("lock_redis");
        $start = time();
        do {
            $lockStr = $redis->eval(self::LUA_LOCK_STR, [$key, $lockValue, $time], 1);
            if ($lockStr !== $lockValue) {
                //加锁失败
                if (time() - $start < $timeOut) {
                    usleep(200000); //微秒 1秒等于1000000
                    continue;
                }
                return false;
            } else {
                break;
            }
        } while ($lockValue !== $lockStr);
        return $lockValue;
    }

    /**
     * 释放锁资源
     *
     * @Author YangHB
     * @DateTime 2022-09-05 17:04:16
     *
     * @param string $key
     * @param string $lockValue 加锁时的锁内容，避免错误释放
     * @param bool $allAbort 直接释放多层锁
     * @return boolean
     */
    public function unLock(string $key, string $lockValue, bool $allAbort = false): bool
    {
        $key = $this->getKey($key);
        $redis = Cache::store("lock_redis");
        $ret = $redis->eval(self::LUA_UN_LOCK_STR, [$key, $lockValue, $allAbort ? 1 : 0], 1);
        if (Str::startsWith($ret, "VALUE:")) {
            return true;
        }
        return $ret;
    }
}
